﻿using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using WholeSlideImageLibrary.DeepZoom;
using WholeSlideImageLibrary.Features;

namespace WholeSlideImageLibrary.Formats.DeepZoomDirectory
{
    internal class DeepZoomDirectoryWholeSlideImage : WholeSlideImage, IDeepZoomProfileHintFeature
    {
        private readonly DirectorySourceInfo? _directorySourceInfo;
        private readonly string _name;
        private readonly DeepZoomManifest _manifest;
        private DeepZoomLayer[]? _layers;
        private EncodedImageFileFormat _format;

        public DeepZoomDirectoryWholeSlideImage(DirectorySourceInfo? directorySourceInfo, string? name, DeepZoomManifest manifest)
        {
            _directorySourceInfo = directorySourceInfo;
            _name = name ?? string.Empty;
            _manifest = manifest;
            _layers = DeepZoomHelper.CalculateDeepZoomLayers(manifest.Width, manifest.Height);

            string format = manifest.Format!;
            if ("jpg".Equals(format, StringComparison.OrdinalIgnoreCase) || "jpeg".Equals(format, StringComparison.OrdinalIgnoreCase))
            {
                _format = EncodedImageFileFormat.Jpeg;
            }
            else if ("png".Equals(format, StringComparison.OrdinalIgnoreCase))
            {
                _format = EncodedImageFileFormat.Png;
            }
            else if ("bmp".Equals(format, StringComparison.OrdinalIgnoreCase))
            {
                _format = EncodedImageFileFormat.Bmp;
            }
            else
            {
                _format = EncodedImageFileFormat.None;
            }
        }

        public override int LevelCount => EnsureNotDisposed().Length;

        public override System.Drawing.Size GetLevelSize(int level)
        {
            DeepZoomLayer[]? layers = EnsureNotDisposed();

            if ((uint)level >= (uint)layers.Length)
            {
                throw new ArgumentOutOfRangeException(nameof(level));
            }

            DeepZoomLayer layer = layers[layers.Length - 1 - level];
            return new System.Drawing.Size(layer.Width, layer.Height);
        }

        public override async ValueTask ReadRegionAsync<T>(int level, int x, int y, int width, int height, Memory<T> buffer, CancellationToken cancellationToken)
        {
            DeepZoomLayer[]? layers = EnsureNotDisposed();

            if ((uint)level >= (uint)layers.Length)
            {
                throw new ArgumentOutOfRangeException(nameof(level));
            }
            if (Unsafe.SizeOf<T>() * buffer.Length < 4 * width * height)
            {
                throw new ArgumentException("destination is too small.", nameof(buffer));
            }
            DirectorySourceInfo? directory = _directorySourceInfo;
            if (directory is null)
            {
                throw new InvalidDataException();
            }

            int layerIndex = layers.Length - 1 - level;
            DeepZoomLayer layer = layers[layerIndex];

            int layerWidth = layer.Width;
            int layerHeight = layer.Height;
            int tileSize = _manifest.TileSize;
            int overlap = _manifest.Overlap;

            int layerColCount = (layerWidth + (tileSize - 1)) / tileSize;
            int layerRowCount = (layerHeight + (tileSize - 1)) / tileSize;

            int colStart = FindTileLeftToRight(x, layerColCount, tileSize, overlap);
            int rowStart = FindTileLeftToRight(y, layerRowCount, tileSize, overlap);
            int colEnd = FindTileRightToLeft(x + width - 1, layerColCount, tileSize, overlap);
            int rowEnd = FindTileRightToLeft(y + height - 1, layerRowCount, tileSize, overlap);

            if (colEnd >= layerColCount)
            {
                colEnd = layerColCount - 1;
            }
            if (rowEnd >= layerRowCount)
            {
                rowEnd = layerRowCount - 1;
            }

            int colLength = colEnd - colStart + 1;
            int rowLength = rowEnd - rowStart + 1;

            int canvasOffsetX = colStart * tileSize - overlap;
            int canvasOffsetY = rowStart * tileSize - overlap;

            int canvasWidth = colLength * tileSize + 2 * overlap;
            int canvasHeight = rowLength * tileSize + 2 * overlap;
            if (canvasWidth == 0 || canvasHeight == 0)
            {
                return;
            }

            string basePath = _name + "_files/";
            string format = _manifest.Format!;
            using var destination = Image.WrapMemory(buffer.Cast<T, Rgba32>(), width, height);

            byte[]? pooledBuffer = null;
            Image<Rgba32>? canvas = null;
            try
            {
                canvas = new Image<Rgba32>(canvasWidth, canvasHeight);

                // 读取所有覆盖的patch
                for (int col = 0; col < colLength; col++)
                {
                    for (int row = 0; row < rowLength; row++)
                    {
                        FileSourceInfo? file = await directory.GetFileAsync($"{basePath}{layerIndex}/{colStart + col}_{rowStart + row}.{format}", cancellationToken).ConfigureAwait(false);
                        if (file is null)
                        {
                            throw new InvalidDataException();
                        }
                        Image<Rgba32> tile;
#if false
                        if (file is IPhysicalFileSource physicalFileSource)
                        {
                            tile = Image.LoadAsync<Rgba32>(physicalFileSource.PhysicalFilePath);
                        }
                        else
                        {
#endif
                        int bytesToRead = checked((int)file.FileSize);
                        ExpandRentedArray(ref pooledBuffer, bytesToRead);
                        FileSourceReader? reader = await file.OpenAsync(cancellationToken).ConfigureAwait(false);
                        try
                        {
                            int bytesRead = await reader.ReadAsync(0, pooledBuffer.AsMemory(0, bytesToRead), cancellationToken).ConfigureAwait(false);
                            if (bytesRead != bytesToRead)
                            {
                                throw new InvalidDataException();
                            }

                            tile = Image.Load<Rgba32>(new MemoryStream(pooledBuffer, 0, bytesToRead));
                        }
                        finally
                        {
                            await reader.DisposeAsync().ConfigureAwait(false);
                        }
#if false
                        }
#endif

                        canvas.Mutate(ctx =>
                        {
                            ctx.DrawImage(tile, new Point(col * tileSize + (col == 0 ? overlap : 0), row * tileSize + (row == 0 ? overlap : 0)), opacity: 1);
                        });
                    }
                }

                // 剪裁
                destination.Mutate(ctx =>
                {
                    ctx.DrawImage(canvas, new Point(canvasOffsetX - x, canvasOffsetY - y), opacity: 1);
                });
            }
            finally
            {
                if (!(pooledBuffer is null))
                {
                    ArrayPool<byte>.Shared.Return(pooledBuffer);
                }
                canvas?.Dispose();

            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static int FindTileLeftToRight(int position, int tileCount, int tileSize, int overlap)
        {
            return overlap == 0 ? position / tileSize : SlowFindTileLeftToRight(position, tileCount, tileSize, overlap);
        }
        [MethodImpl(MethodImplOptions.NoInlining)]
        private static int SlowFindTileLeftToRight(int position, int tileCount, int tileSize, int overlap)
        {
            for (int i = 0; i < tileCount - 1; i++)
            {
                int start = i * tileSize - (i == 0 ? 0 : overlap);
                int end = (i + 1) * tileSize + (i == tileCount - 1 ? 0 : overlap);
                if (position >= start && position < end)
                {
                    return i;
                }
            }
            return tileCount - 1;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static int FindTileRightToLeft(int position, int tileCount, int tileSize, int overlap)
        {
            return overlap == 0 ? position / tileSize : SlowFindTileRightToLeft(position, tileCount, tileSize, overlap);
        }
        [MethodImpl(MethodImplOptions.NoInlining)]
        private static int SlowFindTileRightToLeft(int position, int tileCount, int tileSize, int overlap)
        {
            for (int i = tileCount - 1; i >= 0; i--)
            {
                int start = i * tileSize - (i == 0 ? 0 : overlap);
                int end = (i + 1) * tileSize + (i == tileCount - 1 ? 0 : overlap);
                if (position >= start && position < end)
                {
                    return i;
                }
            }
            return tileCount - 1;
        }

        public IEnumerable<DeepZoomProfile> GetDeepZoomProfileHints()
        {
            if (_format != EncodedImageFileFormat.None)
            {
                yield return new DeepZoomProfile((short)_manifest.TileSize, (short)_manifest.Overlap, _format);
            }
        }

        public ValueTask<DeepZoomTileSource?> OpenDeepZoomAsync(DeepZoomProfile profile, CancellationToken cancellationToken = default)
        {
            EnsureNotDisposed();

            DeepZoomManifest? manifest = _manifest;
            if (profile.TileSize == manifest.TileSize && profile.Overlap == manifest.Overlap && profile.FileFormat == _format)
            {
                return new ValueTask<DeepZoomTileSource?>(new DeepZoomDirectoryDeepZoomTileSource(this, manifest, profile.FileFormat));
            }

            return default;
        }

        public override ValueTask DisposeAsync()
        {
            _layers = null;
            return base.DisposeAsync();
        }

        internal DirectorySourceInfo? GetDirectorySource()
        {
            EnsureNotDisposed();

            return _directorySourceInfo;
        }

        internal async Task<FileSourceInfo?> GetTileAsync(int layer, int col, int row)
        {
            EnsureNotDisposed();

            DirectorySourceInfo? directorySourceInfo = _directorySourceInfo;
            if (directorySourceInfo is null)
            {
                return null;
            }

            string path = $"{_name}_files/{layer}/{col}_{row}.{_manifest.Format}";
            return await directorySourceInfo.GetFileAsync(path).ConfigureAwait(false);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private DeepZoomLayer[] EnsureNotDisposed()
        {
            DeepZoomLayer[]? layers = _layers;
            if (layers is null)
            {
                ThrowObjectDisposedException();
            }
            return layers;
        }

        [DoesNotReturn]
        [MethodImpl(MethodImplOptions.NoInlining)]
        private static void ThrowObjectDisposedException()
        {
            throw new ObjectDisposedException(nameof(DeepZoomDirectoryWholeSlideImage));
        }

        private static void ExpandRentedArray([NotNull] ref byte[]? buffer, int size)
        {
            if (buffer is null)
            {
                buffer = ArrayPool<byte>.Shared.Rent(size);
                return;
            }
            if (buffer.Length < size)
            {
                ArrayPool<byte>.Shared.Return(buffer);
                buffer = ArrayPool<byte>.Shared.Rent(size);
            }
        }

    }
}
